# obsapi.py
import re
import cv2
import numpy as np
import base64
import pyautogui  
import time
import win32gui
import win32con  
import win32process  
import win32api  
import psutil
import subprocess  
import ctypes  
from ctypes import wintypes  
import pydirectinput
import sys

MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW
MAPVK_VK_TO_VSC = 0

# Keyboard Scan Code Mappings
KEYBOARD_MAPPING = {
    'escape': 0x01,
    'esc': 0x01,
    'f1': 0x3B,
    'f2': 0x3C,
    'f3': 0x3D,
    'f4': 0x3E,
    'f5': 0x3F,
    'f6': 0x40,
    'f7': 0x41,
    'f8': 0x42,
    'f9': 0x43,
    'f10': 0x44,
    'f11': 0x57,
    'f12': 0x58,
    'printscreen': 0xB7,
    'prntscrn': 0xB7,
    'prtsc': 0xB7,
    'prtscr': 0xB7,
    'scrolllock': 0x46,
    'pause': 0xC5,
    '`': 0x29,
    '1': 0x02,
    '2': 0x03,
    '3': 0x04,
    '4': 0x05,
    '5': 0x06,
    '6': 0x07,
    '7': 0x08,
    '8': 0x09,
    '9': 0x0A,
    '0': 0x0B,
    '-': 0x0C,
    '=': 0x0D,
    'backspace': 0x0E,
    'insert': 0xD2 + 1024,
    'home': 0xC7 + 1024,
    'pageup': 0xC9 + 1024,
    'pagedown': 0xD1 + 1024,
    # numpad
    'numlock': 0x45,
    'divide': 0xB5 + 1024,
    'multiply': 0x37,
    'subtract': 0x4A,
    'add': 0x4E,
    'decimal': 0x53,
    # KEY_NUMPAD_ENTER: 0x9C + 1024,
    # KEY_NUMPAD_1: 0x4F,
    # KEY_NUMPAD_2: 0x50,
    # KEY_NUMPAD_3: 0x51,
    # KEY_NUMPAD_4: 0x4B,
    # KEY_NUMPAD_5: 0x4C,
    # KEY_NUMPAD_6: 0x4D,
    # KEY_NUMPAD_7: 0x47,
    # KEY_NUMPAD_8: 0x48,
    # KEY_NUMPAD_9: 0x49,
    # KEY_NUMPAD_0: 0x52,
    # end numpad
    'tab': 0x0F,
    'q': 0x10,
    'w': 0x11,
    'e': 0x12,
    'r': 0x13,
    't': 0x14,
    'y': 0x15,
    'u': 0x16,
    'i': 0x17,
    'o': 0x18,
    'p': 0x19,
    '[': 0x1A,
    ']': 0x1B,
    '\\': 0x2B,
    'del': 0xD3 + 1024,
    'delete': 0xD3 + 1024,
    'end': 0xCF + 1024,
    'capslock': 0x3A,
    'a': 0x1E,
    's': 0x1F,
    'd': 0x20,
    'f': 0x21,
    'g': 0x22,
    'h': 0x23,
    'j': 0x24,
    'k': 0x25,
    'l': 0x26,
    ';': 0x27,
    "'": 0x28,
    'enter': 0x1C,
    'return': 0x1C,
    'shift': 0x2A,
    'shiftleft': 0x2A,
    'z': 0x2C,
    'x': 0x2D,
    'c': 0x2E,
    'v': 0x2F,
    'b': 0x30,
    'n': 0x31,
    'm': 0x32,
    ',': 0x33,
    '.': 0x34,
    '/': 0x35,
    'shiftright': 0x36,
    'ctrl': 0x1D,
    'ctrlleft': 0x1D,
    'win': 0xDB + 1024,
    'winleft': 0xDB + 1024,
    'alt': 0x38,
    'altleft': 0x38,
    ' ': 0x39,
    'space': 0x39,
    'altright': 0xB8 + 1024,
    'winright': 0xDC + 1024,
    'apps': 0xDD + 1024,
    'ctrlright': 0x9D + 1024,
    'up': MapVirtualKey(0x26, MAPVK_VK_TO_VSC),
    'left': MapVirtualKey(0x25, MAPVK_VK_TO_VSC),
    'down': MapVirtualKey(0x28, MAPVK_VK_TO_VSC),
    'right': MapVirtualKey(0x27, MAPVK_VK_TO_VSC),
}


class MouseInput(ctypes.Structure):
    _fields_ = [("dx", ctypes.c_long),  # 水平方向的绝对位置/相对移动量(像素), dwFlags 中包含 MOUSEEVENTF_ABSOLUTE 标识就是绝对移动, 否则是相对移动
                ("dy", ctypes.c_long),  # 垂直方向的绝对位置/相对移动量(像素)
                ("mouseData", ctypes.c_ulong),  # 某些事件的额外参数, 如: MOUSEEVENTF_WHEEL(中键滚动), 可填正负值, 一个滚动单位是120(像素?); 还有 MOUSEEVENTF_XDOWN/MOUSEEVENTF_XUP
                ("dwFlags", ctypes.c_ulong),  # 事件标识集, 可以是移动或点击事件的合理组合, 即可以一个命令实现移动且点击
                ("time", ctypes.c_ulong),  # 事件发生的时间戳, 可以指定发生的时间? 传入0则使用系统提供的时间戳
                ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong))]  # 应用可通过 GetMessageExtraInfo 来接收通过此参数传递的额外消息


class KeyboardInput(ctypes.Structure):
    _fields_ = [("wVk", ctypes.c_ushort),  # 虚拟键码, 范围在[1,254], 如果dwFlags指定了KEYEVENTF_UNICODE, 则wVk必须是0, https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
                ("wScan", ctypes.c_ushort),  # 键的硬件扫描码, 如果dwFlags指定了KEYEVENTF_UNICODE, 则wScan需为一个Unicode字符
                ("dwFlags", ctypes.c_ulong),  # 事件标识集, 可合理组合
                ("time", ctypes.c_ulong),
                ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong))]


class HardwareInput(ctypes.Structure):
    _fields_ = [("uMsg", ctypes.c_ulong),
                ("wParamL", ctypes.c_short),
                ("wParamH", ctypes.c_ushort)]


class Inner(ctypes.Union):  # 共用体, 和结构体类似, 但是各成员属性共用同一块内存空间, 实例化时的空间大小就是成员属性中最大的那个的空间大小, 实例化时只能赋值ki/mi/hi中的一个
    _fields_ = [("ki", KeyboardInput),
                ("mi", MouseInput),
                ("hi", HardwareInput)]


class Input(ctypes.Structure):
    _fields_ = [("type", ctypes.c_ulong),  # 输入事件类型
                ("ii", Inner)]


"""
MouseInput
"""
MOUSEEVENTF_MOVE = 0x0001  # 移动
MOUSEEVENTF_LEFTDOWN = 0x0002  # 左键按下
MOUSEEVENTF_LEFTUP = 0x0004  # 左键释放
MOUSEEVENTF_RIGHTDOWN = 0x0008
MOUSEEVENTF_RIGHTUP = 0x0010
MOUSEEVENTF_MIDDLEDOWN = 0x0020
MOUSEEVENTF_MIDDLEUP = 0x0040
MOUSEEVENTF_XDOWN = 0x0080  # 侧键按下
MOUSEEVENTF_XUP = 0x0100  # 侧键释放
MOUSEEVENTF_WHEEL = 0x0800  # 滚轮垂直滚动, mouseData需传入滚动值, 一个滚动单位是120(像素?)
MOUSEEVENTF_HWHEEL = 0x1000  # 滚轮水平滚动
MOUSEEVENTF_MOVE_NOCOALESCE = 0x2000  # 移动消息不会被合并
MOUSEEVENTF_VIRTUALDESK = 0x4000  # Maps coordinates to the entire desktop. Must be used with MOUSEEVENTF_ABSOLUTE.
MOUSEEVENTF_ABSOLUTE = 0x8000  # 鼠标移动事件, 如果设置此标记就是绝对移动, 否则就是相对移动. 相对鼠标运动受鼠标速度(控制面板中的指针移动速度)和两个鼠标阈值(?和速度在同一个地方设置)的影响, 绝对值移动时范围是[0,65535]

XBUTTON1 = 0x0001  # 侧下键, dwFlags为MOUSEEVENTF_XDOWN/MOUSEEVENTF_XUP时, mouseData需传入x键值
XBUTTON2 = 0x0002  # 侧上键

WHEEL_DELTA = 120  # 鼠标滚轮滚动一个单位的最小值

# dwFlags 不能同时包含 MOUSEEVENTF_WHEEL/MOUSEEVENTF_XDOWN/MOUSEEVENTF_XUP, 因为它们都需要使用mouseData字段. 如果不包含这3个字段, 则mouseData应该传入0
# dwFlags 仅仅能表示状态的变化, 而不能表示状态的持续, 如它能表示按下左键, 但按下左键不代表持续按住左键, 但我测下来好像可以(只发送down不发送up则相当于一直按住)?
# If dwFlags contains MOUSEEVENTF_WHEEL, then mouseData specifies the amount of wheel movement. A positive value indicates that the wheel was rotated forward, away from the user; a negative value indicates that the wheel was rotated backward, toward the user. One wheel click is defined as WHEEL_DELTA, which is 120.

"""
KeyboardInput
"""
KEYEVENTF_EXTENDEDKEY = 0x0001  # If specified, the scan code was preceded by a prefix byte that has the value 0xE0 (224).
KEYEVENTF_KEYUP = 0x0002  # 指定该标记, 就是按键弹起, 否则就是按键按下
KEYEVENTF_SCANCODE = 0x0008  # 指定该标记, 就是使用硬件扫描码来发送按键按下事件, wVk参数将被忽略
KEYEVENTF_UNICODE = 0x0004  # 发送Unicode字符消息(按下), 可以与KEYEVENTF_KEYUP组合使用

# 按键的虚拟键码和扫描代码是两套码, 虚拟键码可能因键盘/布局不同而不同, 但扫描代码是键盘无关的, 所以推荐使用扫描码
# dwFlags 仅仅能表示状态的变化, 而不能表示状态的持续, 如按下w键, 物理键盘会持续输入w, 而模拟键盘则仅能输入一个w

"""
Input
"""
INPUT_MOUSE = 0  # 鼠标输入事件
INPUT_KEYBOARD = 1  # 键盘输入事件
INPUT_HARDWARE = 2  # 硬件输入事件

"""
SendInput
cInputs: pInputs 数组的个数, 即SendInput可以一下发好几个鼠标事件/键盘事件, 这些事件存储在一个连续的数组空间里
pInputs: 输入事件的实例的指针
cbSize: 每一个INPUT事件的结构体空间, 鼠标事件和键盘事件应该不能同时放到pInputs数组中, 因为他们的size不同 
该函数返回成功插入键盘或鼠标输入流的事件数。如果函数返回零，则输入已被另一个线程阻塞。要获取扩展错误信息，请调用GetLastError.
"""


def SendInput(*inputs):  # 接收任意个参数, 将其打包成为元组形参, 双*是打包成为字典形参
    nInputs = len(inputs)
    pointer = Input * nInputs
    pInputs = pointer(*inputs)
    # 创建一个指定类型的C数组并返回首指针, 格式如下
    # pointer = (Type * 数组长度)(填充该数组的实例,用逗号分隔开,个数不超过数组长度)
    # 1. pointer = (Input * 3)(); 创建一个空间为3个Input的C数组, 其中的每个位置的Input已经按默认值初始化
    # 2. pointer = (Input * 3)(input); 创建一个空间为3个Input的C数组, 并将input赋值给第一个位置
    # 3. pointer = (Input * 3)(input, input2, input2), 创建一个空间为3个Input的C数组, 并将input赋值给第一个位置, 将input2赋值给第二第三个位置
    # 4. pointer = (Input * 3)(input, input2, input3, input4), 创建一个空间为3个Input的C数组, 赋值时报错, 因为没有放input4的空间
    # 5. pointer = (Input * 3)(*inputs); 创建一个空间为3个Input的C数组, 并将集合类型的inputs解包并赋值给数组的对应位置
    # 6. 也可以如上分开两行写
    cbSize = ctypes.sizeof(Input)
    return ctypes.windll.user32.SendInput(nInputs, pInputs, cbSize)


class Keyboard:

    @staticmethod
    def press(wVk):  # 十六进制虚拟键码, 范围在[1,254], 如果dwFlags指定了KEYEVENTF_UNICODE, 则wVk必须是0, https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
        return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(wVk, 0, 0, 0, None))))

    @staticmethod
    def release(wVk):
        return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(wVk, 0, KEYEVENTF_KEYUP, 0, None))))

    @staticmethod
    def pressByScanCode(wScan):  # 十六进制键扫描码, 可通过该工具获得, https://github.com/Lateralus138/Key-ScanCode/releases/tag/1.9.30.18
        return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(0, wScan, KEYEVENTF_SCANCODE, 0, None))))

    @staticmethod
    def releaseByScanCode(wScan):
        return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(0, wScan, KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP, 0, None))))

    @staticmethod
    def pressByUnicode(wScan):  # 十六进制Unicode编码, 比如 汉字 "一" 的十六进制UTF8编码为 0x4e00, 则可通过发送该编码直接输入汉字一
        return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(0, wScan, KEYEVENTF_UNICODE, 0, None))))

    @staticmethod
    def releaseByUnicode(wScan):
        return SendInput(Input(INPUT_KEYBOARD, Inner(ki=KeyboardInput(0, wScan, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, None))))


class OBSApi:
    def __init__(self):
        self.small_image_cache = {} #本地图片缓存
        self.x = 0
        self.y = 0
        self.vk_codes = KEYBOARD_MAPPING

    # 获取桌面画面
    # 返回值：opencv的image对象
    def get_screenshot_image(self):
        # 使用 PyAutoGUI 截取全屏截图  
        screenshot = pyautogui.screenshot()  
  
        # 将截图转换为 numpy 数组  
        # 注意：PyAutoGUI 返回的图像是 PIL 图像对象，需要转换为 RGB 格式的 numpy 数组  
        frame = np.array(screenshot)  
  
        # 将图像从 RGB 格式转换为 BGR 格式，因为 OpenCV 使用 BGR 格式  
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)  
  
        # 返回 OpenCV 图像对象  
        return frame  

    # 获取游戏画面
    # 返回值：opencv的image对象
    def get_gamecapture_image(self, hWnd):  
        # 获取游戏窗口的位置和大小  
        left, top, width, height = self.get_game_window_rect(hWnd)  
          
        # 使用pyautogui截取屏幕指定区域  
        screenshot = pyautogui.screenshot(region=(left, top, width, height))  
          
        # 将PIL图像转换为numpy数组  
        frame = np.array(screenshot)  
          
        # 将图像从RGB转换为BGR，因为OpenCV使用BGR格式  
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)  
          
        return frame  

    # 设置鼠标位置（在整个桌面中的全局位置）  
    # 返回值：result  
    # result为True时成功，为False时失败  
    # set_cursor_pos_on_game
    def set_cursor_pos(self, x, y):  
        try:  
            win32api.SetCursorPos((int(x),int(y)))
            # pydirectinput.moveTo(int(x), int(y), duration=0)  # duration=0 表示立即移动  
            return True  
        except Exception as e:  
            print(f"设置鼠标位置失败: {e}")  
            return False  
  
    # 获取鼠标位置（在整个桌面中的全局位置）  
    # 返回值：result, x, y  
    # result为True时成功，为False时失败。result为True时x、y才有效  
    def get_cursor_pos(self):  
        try:  
            x, y = pydirectinput.position()  
            return True, x, y  
        except Exception as e:  
            print(f"获取鼠标位置失败: {e}")  
            return False, None, None  
  
    # 模拟鼠标左键按下  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_left_down(self):  
        try:  
            win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,self.x,self.y,0,0)
            return True  
        except Exception as e:  
            print(f"模拟鼠标左键按下失败: {e}")  
            return False  
  
    # 模拟鼠标左键弹起  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_left_up(self):  
        try:  
            # pydirectinput.mouseUp(button='left')  
            win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,self.x,self.y,0,0)
            return True  
        except Exception as e:  
            print(f"模拟鼠标左键弹起失败: {e}")  
            return False  
  
    # 模拟鼠标左键点击（按下+弹起）  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_left_click(self):  
        try:  
            # pydirectinput.click(button='left')  
            win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,self.x,self.y,0,0)
            win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,self.x,self.y,0,0)
            return True  
        except Exception as e:  
            print(f"模拟鼠标左键点击失败: {e}")  
            return False  
  
    # 模拟鼠标右键按下  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_right_down(self):  
        try:  
            # pydirectinput.mouseDown(button='right')  
            win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTDOWN, self.x,self.y, 0, 0)
            return True  
        except Exception as e:  
            print(f"模拟鼠标右键按下失败: {e}")  
            return False  
  
    # 模拟鼠标右键抬起  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_right_up(self):  
        try:  
            # pydirectinput.mouseUp(button='right')  
            win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP, self.x,self.y, 0, 0)
            return True  
        except Exception as e:  
            print(f"模拟鼠标右键抬起失败: {e}")  
            return False  
  
    # 模拟鼠标右键点击（按下+弹起）  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_right_click(self):  
        try:  
            # pydirectinput.click(button='right') 
            win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTDOWN, self.x,self.y, 0, 0) 
            win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP, self.x,self.y, 0, 0)
            return True  
        except Exception as e:  
            print(f"模拟鼠标右键点击失败: {e}")  
            return False  

    # 是否锁定大写
    def is_caps_lock_on(self):
        hllDll = ctypes.WinDLL("User32.dll")
        VK_CAPITAL = 0x14
        return hllDll.GetKeyState(VK_CAPITAL) & 0xffff != 0
    
    # 锁定大小写
    # state 开关
    def set_caps_lock(self , state):
        # Get the current state of Caps Lock
        is_on = ctypes.WinDLL("User32.dll").GetKeyState(0x14) & 0xffff != 0

        # If the desired state is different from the current state, toggle Caps Lock
        if is_on != state:
            ctypes.WinDLL("User32.dll").keybd_event(0x14, 0, 0, 0)
            ctypes.WinDLL("User32.dll").keybd_event(0x14, 0, 2, 0)

    # 输入字符串，并模拟键盘输入  
    def input_string(self, account_string):  
        for char in account_string:
            shift_pressed = self.is_caps_lock_on()  
            if char.isupper(): # 如果字符是大写  
                if not shift_pressed: # 如果Shift键尚未按下  
                    self.set_caps_lock(True)
            elif char.islower(): # 如果字符是小写
                if shift_pressed: # 如果Shift键尚未按下  
                    self.set_caps_lock(False)
            self.sim_key_click(self.get_vk_code(char.lower()))


    # 模拟键盘输入  
    # wScan：十六进制键扫描码，bDown=1时为按下，bDown=0时为弹起  
    # 返回值为True时成功，为False时失败  
    def sim_key_input(self , wScan, bDown):
        if bDown == 1:
            Keyboard.pressByScanCode(wScan)
        else:
            Keyboard.releaseByScanCode(wScan)

    # 模拟键盘点击（按下+弹起）
    # wScan：  十六进制键扫描码
    # 返回值为True时成功，为False时失败  
    def sim_key_click(self, wScan):  
        try:  
            Keyboard.pressByScanCode(wScan)
            Keyboard.releaseByScanCode(wScan)
            return True  
        except Exception as e:  
            print(f"模拟键盘点击失败: {e}")  
            return False  

             

    # 获取字符对应的虚拟键码  
    #  
    # 参数:  
    #     char (str): 要获取虚拟键码的字符  
    #  
    # 返回值:  
    #     str: 字符对应的虚拟键码，如果未找到则返回 None  
    def get_vk_code(self, char):  
        # 使用类初始化时创建的映射表  
        return self.vk_codes.get(char.lower())  # 不区分大小写  
        
    # WindowsAPI: FindWindowW
    # className 为字符串'null'时表示传NULL,以'REGEX:'开头时启用正则表达式
    # windowName 为字符串'null'时表示传NULL,以'REGEX:'开头时启用正则表达式
    # find 是否使用匹配模式，默认为True
    # 返回值为hWnd（窗口句柄），找不到窗口时返回0
    def find_window(self, className, windowName, find=True):
         # 获取所有窗口句柄、标题和类名
        def callback(hwnd, extra):
            if win32gui.IsWindowVisible(hwnd):
                window_title = win32gui.GetWindowText(hwnd)
                window_class = win32gui.GetClassName(hwnd)
                if window_class == className:
                    extra.append((hwnd, window_title, window_class))

        windows = []
        win32gui.EnumWindows(callback, windows)
        
        for hwnd, window_title, window_class in windows:
            if not find:
                if window_title == windowName: # 全部匹配
                    return hwnd
            elif re.search(windowName, window_title): # 部分匹配
                return hwnd
        return 0


        
    # WindowsAPI: GetWindowRect 获取窗口区域
    # 第一个参数为hWnd
    # 返回值为BOOL 和 [left,top,right,bottom]
    def get_game_window_rect(self, hWnd):  
        rect = win32gui.GetWindowRect(hWnd)  
        if rect:
            return rect
        
        return None

        
    # WindowsAPI: GetClientRect 获取窗口Client区域
    # 第一个参数为hWnd
    # 返回值为BOOL,和left,top,right,bottom
    def get_client_rect(self, hWnd):  
        rect = win32gui.GetClientRect(hWnd)  
        return (True, rect) if rect else (False, (0, 0, 0, 0))  # 如果获取失败，返回False和默认的矩形

        
    # 相当于调用system(command)在Windows的cmd.exe中执行命令
    # 第一个参数为command字符串
    # 返回值为system命令的返回值（一般是命令的%ERRORLEVEL%）
    def exec_system(self, command):
        try:
            # Execute the command in cmd.exe
            result = subprocess.Popen(command)
            # Return the command's return code
            return result.returncode
        except Exception as e:
            print(f"Error executing command: {e}")
            return -1

    # 设置窗口为置顶窗口
    # 参数为：窗口句柄
    # 返回值为BOOL
    def set_window_topmost(self, hWnd):
        return win32gui.SetForegroundWindow(hWnd)
        # return win32gui.SetWindowPos(hWnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE) 
        
    # 设置窗口位置
    # 参数为：窗口句柄, x, y
    # 返回值为BOOL
    def set_window_pos(self, hWnd, x, y):  
        return win32gui.SetWindowPos(hWnd, 0, x, y, 0, 0, win32con.SWP_NOSIZE)  


    # 设置窗口大小
    # 参数为：窗口句柄, w, h
    # 返回值为BOOL
    def set_window_size(self, hWnd, w, h):  
        return win32gui.SetWindowPos(hWnd, 0, 0, 0, w, h, win32con.SWP_NOMOVE)  


    # 设置窗口位置和大小
    # 参数为：窗口句柄, x,y, w, h
    # 返回值为BOOL
    def set_window_pos_size(self, hWnd, x, y, w, h):  
        return win32gui.SetWindowPos(hWnd, 0, x, y, w, h, 0)  


    #判断窗口句柄是否有效
    #需要30.2.0-rc15
    def is_window(self, hWnd):
        return win32gui.IsWindow(hWnd)  
        
    #判断窗口是否可见
    #需要30.2.0-rc15
    def is_window_visible(self, hWnd):
        return win32gui.IsWindowVisible(hWnd)  
    
    #判断窗口是否卡死
    #需要30.2.0-rc11
    def is_window_ghosted(self, hWnd):
        pass
    
    #获取窗口所属PID
    #需要30.2.0-rc11
    def get_window_process(self, hWnd):  
        # 获取窗口所属的线程ID  
        thread_id, process_id = win32process.GetWindowThreadProcessId(hWnd)  
        return process_id  

    
    #强制结束进程
    #需要30.2.0-rc11
    def terminate_process(self, pid):  
        handle = win32api.OpenProcess(win32con.PROCESS_TERMINATE, False, pid)  
        win32api.TerminateProcess(handle, 0)  
        win32api.CloseHandle(handle)  
    
    #根据进程名查找进程id
    #找不到时返回0
    #需要30.2.0-rc13
    def find_process_id_by_name(self, name):  
        # 遍历所有进程，查找与名称匹配的进程ID  
        for proc in psutil.process_iter(['pid', 'name']):  
            if proc.info['name'] == name:  
                return proc.info['pid']  
        return 0  

    
    # 相当于调用ShellExecuteW
    # param和curdir可以为'null'
    # 返回值为BOOL
    #需要30.2.0-rc16
    def shell_execute(self, command, param=None, curdir=None):  
        # 使用subprocess库执行shell命令  
        try:  
            if param:  
                command = f"{command} {param}"  
            if curdir:  
                subprocess.run(command, shell=True, cwd=curdir, check=True)  
            else:  
                subprocess.run(command, shell=True, check=True)  
            return 1  # 成功  
        except subprocess.CalledProcessError:  
            return 0  # 失败  


    # 切换键盘布局
    # layout: 
    # 美式英语："00000409"
    # 英国英语："00000809"
    # 中文（简体）："00000804"
    # 中文（繁体）："00000404"
    # 返回值为BOOL：0 / 1
    # 需要30.2.0-rc17
    def switch_keyboard_layout(self, layout):  
        # 使用win32api切换键盘布局  
        try:    
            # ctypes.windll.user32.LoadKeyboardLayoutW
            new_layout = win32api.LoadKeyboardLayout(layout, win32con.KLF_ACTIVATE)
            print(new_layout)
    
            # LoadKeyboardLayout = ctypes.windll.user32.LoadKeyboardLayoutW  
            # LoadKeyboardLayout(layout, wintypes.KLF_ACTIVATE)  
            return 1  # 成功  
        except Exception as e:  
            # 可以打印异常信息或记录日志
            print(e)
            return 0  # 失败  

        
    # 切换目标窗口的键盘布局
    # hWnd: 窗口句柄
    # layout: 
    # 美式英语："00000409"
    # 英国英语："00000809"
    # 中文（简体）："00000804"
    # 中文（繁体）："00000404"
    # 返回值为BOOL：0 / 1
    # 需要30.2.0-rc19
    def switch_keyboard_layout_for_window(self, hWnd, layout): 
        user32 = ctypes.WinDLL('user32', use_last_error=True) 
        # Load the Layout
        hkl = user32.LoadKeyboardLayoutW(layout, 1)
        if not hkl:
            print("Failed to load keyboard layout.")
            return 0

        # Activate the Layout for the given window
        result = user32.SendMessageW(hWnd, 0x0050, 0, hkl)
        if not result:
            print("Failed to switch keyboard layout.")
            return 0

        return 1


    # 等价于：SendMessageA(hWnd, Msg, wParam, lParam)
    # 需要30.2.0-rc18
    def send_message_to_window(self, hWnd, Msg, wParam, lParam):  
        # 使用win32api发送消息到窗口  
        try:  
            result = win32api.SendMessage(hWnd, Msg, wParam, lParam)  
            return result  
        except Exception as e:  
            # 可以打印异常信息或记录日志  
            return 0  # 失败  

    def find_image_in_screenshot(self, large_image, small_image_path , Sim):
        """
        在当前截图中查找小图的位置及其窗口矩形。

        Args:
            small_image_path (str): 小图的文件路径。

        Returns:
            tuple or None: 包含小图在截图中位置的左上角坐标(x, y)和窗口矩形(x, y, width, height)的元组，如果未找到或发生错误则返回None。
        """
        try:
            # 检查小图是否已经在缓存中
            if small_image_path in self.small_image_cache:
                small_image = self.small_image_cache[small_image_path]
            else:
                # 读取小图并存储到缓存中
                small_image = cv2.imread(small_image_path)  # 以彩色模式读取小图
                if small_image is None:
                    raise ValueError("无法加载小图图片")
                self.small_image_cache[small_image_path] = small_image

            # 使用matchTemplate寻找小图在大图中的位置
            result = cv2.matchTemplate(large_image, small_image, cv2.TM_CCOEFF_NORMED)
            threshold = 0.8  # 设置匹配阈值
            if Sim :
                threshold = Sim  # 设置匹配阈值
                
            loc = np.where(result >= threshold)

            # 如果没有找到匹配项,则直接返回None
            if not loc[0].size:
                return None

            # 找到最佳匹配位置(即result中的最大值位置)
            min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)


            # 提取最佳匹配的坐标
            pt = (max_loc[0], max_loc[1])

            # 计算小图在大图中的窗口矩形
            small_image_height, small_image_width, _ = small_image.shape
            window_rect = (pt[0], pt[1], small_image_width, small_image_height)

            return (window_rect)

        except Exception as e:
            print(f"在查找小图时发生错误: {str(e)}")
            return None

# if  __name__ == "__main__":
#     api=OBSApi()
#     # 模拟按下'A'键
#     time.sleep(1)
#     Keyboard.pressByScanCode(0x2A)
#     pydirectinput.press('shift')
#     # while True:
#     #     time.sleep(1)
#     #     pydirectinput.keyDown('shift')  
  
#     #     # 等待一段时间，这里设置为2秒，可以根据需要调整  
#     #     time.sleep(0.5)  
        
#     #     # 释放 'shift' 键  
#         # pydirectinput.keyUp('shift')

